Ana içeriğe geç
  1. 100 Günde SwiftUI Notları/

10.Gün - Swift Struct - 1 : Struct, Computed Property ve Property Observer

Struct Nasıl Oluşturulur? #

Swift’teki struct, kendi değişkenleri ve kendi fonksiyonları olan kapsamlı ve kendimize özel veri türü oluşturmamıza olanak sağlar.

Basitçe bir struct aşağıdaki gibi gözükür;

struct Album {
    let title: String
    let artist: String
    let year: Int

    func printSummary() {
        print("\(title) (\(year)) by \(artist)")
    }
}

Yukarıdaki kod, Album adında yeni bir tür (type) oluşturur. Bu tür, title ve artist olmak üzere iki adet String, year adında bir Int ve printSummary() adında bir fonksiyon içerir.

Struct’ın isimlendirmesini yaparken ilk harfi büyük yazılır. Bizim örneğimizdeki Album ’ün de büyük harfle başladığına dikkat edelim.

Artık, Album tipinde değişken ve sabitler oluşturabilir, değer atayabilir veya kopyalama işlemlerini yapabiliriz. Tıpkı daha önce String veya Int oluşturduğumuz gibi.

let red = Album(title: "Red", artist: "Taylor Swift", year: 2012)
let wings = Album(title: "Wings", artist: "BTS", year: 2016)

print(red.title)
print(wings.artist)

red.printSummary()
wings.printSummary()

//ÇIKTI:
//----------------------------------------
//Red
//BTS
//Red (2012) by Taylor Swift
//Wings (2016) by BTS

Fonksiyon çağırır gibi yeni Album türünde veriler oluşturabiliyoruz. Gördüğünüz gibi hem red hem de wings Album türünde olmasına rağmen birbirlerinden tamamen ayrılar.

Bu durumu her struct üzerinde printSummary() fonksiyonunu çağırdığımızda görebiliriz. Aynı struct’tan türemiş red ve wing üzerinde printSummary() fonksiyonunu çağırdığımızda bize farklı değerler döndürür.

İşlerin farklılaştığı yer, değişebilen değerlere sahip olmak istediğimiz zamandır. Örneğin gerektiğinde tatil yapabilen bir Employee struct oluşturabiliriz.

struct Employee {
    let name: String
    var vacationRemaining: Int

    func takeVacation(days: Int) {
        if vacationRemaining > days {
            vacationRemaining -= days
            print("I'm going on vacation!")
            print("Days remaining: \(vacationRemaining)")
        } else {
            print("Oops! There aren't enough days remaining.")
        }
    }
}

Yukarıdaki kodu yazmak istediğimizde, Swift kodu oluşturmak istemeyecektir.

struct_mutable_function

Swift struct’ları ve sahip oldukları verileri sabit olarak oluşturur. Bu Swift’in daha hızlı çalışmasını sağlar. Fakat struct içerisindeki bir fonksiyon, bir veriyi değiştirmek istediğinde buna izin verilmez, çünkü tüm veriler sabitti.

Sonuçta, struct’a ait veriyi değiştiren bir fonksiyonumuz varsa bu durum mutating ile işaretlenmelidir. Veriyi değiştirmeyen sadece okuyan bir fonksiyonumuz varsa, işini sorunsuzca yapabilir mutating ile işaretlenmesi gerekmez.

mutating func takeVacation(days: Int) {

Struct kodumuzda yukarıdaki değişikliği yaptıktan sonra gayet iyi çalışacaktır. Employee struct’tan bir sabit oluşturalım:

let archer = Employee(name: "Sterling Archer", vacationRemaining: 14)
archer.takeVacation(days: 5)
print(archer.vacationRemaining)

Yukarıdaki kodu oluşturmak istediğimizde yine hata alacağız. Çünkü mutating fonksiyon barındıran bir struct’ı sabit olarak tanımladığımızda veri değiştirilemez.

mutable function and let

Çalışabilecek kod aşağıdaki gibi olmalıdır.

var archer = Employee(name: "Sterling Archer", vacationRemaining: 14)
archer.takeVacation(days: 5)
print(archer.vacationRemaining)

Struct ile ilgili önemli noktalar;

  • Struct’a ait sabit ve değişkenler property olarak adlandırılır.
  • Struct’a ait fonksiyonlar method olarak adlandırılır.
  • Bir struct’tan bir sabit veya değişken oluşturulduğuna buna instance denilmektedir.
  • Struct’tan bir instance oluştururken aşağıdaki gibi bir initializer kullanırız.
    • Album(title: "Wings", artist: "BTS", year: 2016)

Bir struct’tan instance oluştururken, aslında init fonksiyonu kullanılır. Fakat Swift bu işlemi bizim yerimize yaptığı için bir ayrıca init yazarak instance oluşturmayız. Bu duruma syntactic sugar adı verilmektedir. Aşağıdaki kod örneklerinin ikiside aynı şeyi yapar, ve aynı şekilde Employee struct’tan instance oluşturur.

var archer1 = Employee(name: "Sterling Archer", vacationRemaining: 14)
var archer2 = Employee.init(name: "Sterling Archer", vacationRemaining: 14)

init() fonksiyonu bizim struct’ı oluştururken belirlediğimiz property ‘lere göre otomatik olarak oluşturulmaktadır.

Örneğin struct’ımızda 2 adet property’imiz olsun;

let name: String
var vacationRemaining = 14

Swift vacationRemaining için, eğer init esnasında belirtmezsek, varsayılan olarak 14 değerini atayacaktır.

let kane = Employee(name: "Lana Kane")
let poovey = Employee(name: "Pam Poovey", vacationRemaining: 35)

Fakat burada dikkat edilecek önemli bir nokta var: Eğer herhangi bir struct property’sini sabit (let) olarak tanımlayıp değer atarsak, bu property init fonksiyonu içinde gözükmeyecektir. (Çünkü bir sabite değer ataması bir kez yapılabilir.) Varsayılan değer ataması kullanabilmek için mutlaka değişken (var) kullanmalıyız.

Struct Computed Property #

Struct’lar iki çeşit property ’ye sahip olabilir.

Stored Property : Struct’ın bir instance ‘ı içinde bir veri parçasını tutan değişken (var) veya sabittir (let)

Computed Property : Her erişildiğinde property ‘nin değeri dinamik olarak hesaplanır. Bu durum computed property ‘yi, stored property ile fonksiyonun karışımı haline getirir. Stored property gibi erişilir fakat fonksiyon gibi çalışır.

Daha önceki Employee struct’ımızın basitleştirilmiş halini örneğimiz olarak kullanalım.

struct Employee {
    let name: String
    var vacationRemaining: Int
}

var archer = Employee(name: "Sterling Archer", vacationRemaining: 14)
archer.vacationRemaining -= 5
print(archer.vacationRemaining)
archer.vacationRemaining -= 3
print(archer.vacationRemaining)

//ÇIKTI:
//----------------------------------------
//9
//6

Yukarıdaki struct işe yarıyor fakat, bazı değerli bilgileri kaybediyoruz. Struct’ı oluştururken çalışanın izin hakkı sayısını vacationRemainig değişkeninde tutmuştuk. Fakat çalışan izin kullandıkça çalışanın izin hakkı bilgisini kaybediyoruz.

Bu problemin üstesinden computed property kullanarak gelebiliriz.

struct Employee {
    let name: String
    var vacationAllocated = 14
    var vacationTaken = 0

    var vacationRemaining: Int {
        vacationAllocated - vacationTaken
    }
}

Artık vacationRemaining ’i doğrudan atamak yerine, izin hakkından kullanılan izni çıkararak hesaplıyoruz.

vacationRemaining ‘i öğrenmek istediğimizde standart stored property gibi okuyabiliyoruz.

var archer = Employee(name: "Sterling Archer", vacationAllocated: 14)
archer.vacationTaken += 4
print(archer.vacationRemaining)
archer.vacationTaken += 4
print(archer.vacationRemaining)

//ÇIKTI:
//----------------------------------------
//10
//6

Bu gerçekten güçlü bir özellik. Normal bir property olarak gözüküyor fakat biz okumak istediğimizde hesaplamalar yapılıyor.

ÖNEMLİ NOT : Sabitler (let) computed property olamazlar. (Neden? çünkü sabitler 😁 değeleri bir kez atanabilir.)

Fakat vacationRemaining property’sine veri yazamayız, çünkü Swift’e bunu nasıl yapması gerektiğini söylemedik. Bunu yapabilmek için bu property de getter ve setter sağlamalıyız. Getter değeri okuyan kod, Setter ise değeri yazan kod anlamına gelmektedir.

Employee struct’ımıza getter ve setter ekleyelim.

var vacationRemaining: Int {
    get {
        vacationAllocated - vacationTaken
    }

    set {
        vacationAllocated = vacationTaken + newValue
    }
}

get ve set yukarıdaki örnekteki gibi yazılabilmektedir. Burada önemli olan newValue. Bu bize Swift tarafından otomatik olarak sağlanır ve kullanıcının property ‘ye atamak istediği değeri saklar.

getter ve setter ‘ı sağladıktan sonra vacationRemaining ’i istediğimiz gibi değiştirebiliriz.

var archer = Employee(name: "Sterling Archer", vacationAllocated: 14)
archer.vacationTaken += 4
archer.vacationRemaining = 5
print(archer.vacationAllocated)

//ÇIKTI:
//----------------------------------------
//9

Property Observer #

Property değiştiğinde çalışan kod parçalarına property observer denilmektedir. Property observer iki şekilde olabilir: Property değiştiğinde didSet , property değişmeden önce willSet observer’ı çalışır.

Property obsever ‘a neden ihtiyaç duyacağımızı anlamak için aşağıdaki gibi bir kod düşünelim;

struct Game {
    var score = 0
}

var game = Game()
game.score += 10
print("Score is now \(game.score)")
game.score -= 3
print("Score is now \(game.score)")
game.score += 1

//ÇIKTI:
//----------------------------------------
//Score is now 10
//Score is now 7

Yukarıdaki kodda, score property’si değiştiriliyor ve her değişimden sonra güncel score print ile yazdırılıyor. Fakat bir problem var: en son score değişikliğinden sonra herhangi birşey yazdırılmıyor.

Aynı kodu property observer ile birlikte yazalım.

struct Game {
    var score = 0 {
        didSet {
            print("Score is now \(score)")
        }
    }
}

var game = Game()
game.score += 10
game.score -= 3
game.score += 1

//ÇIKTI:
//----------------------------------------
//Score is now 10
//Score is now 7
//Score is now 8

Ayrıca didSet içinde kullanılabilen ve Swift tarafından sağlanan oldValue sabitide bulunmaktadır. Tabiki willSet içinde otomatik sağlanan newValue sabiti de vardır.

struct App {
    var contacts = [String]() {
        willSet {
            print("Current value is: \(contacts)")
            print("New value will be: \(newValue)")
        }

        didSet {
            print("There are now \(contacts.count) contacts.")
            print("Old value was \(oldValue)")
        }
    }
}

var app = App()
app.contacts.append("Adrian E")
app.contacts.append("Allen W")
app.contacts.append("Ish S")

//ÇIKTI:
//----------------------------------------
//Current value is: []
//New value will be: ["Adrian E"]
//There are now 1 contacts.
//Old value was []
//Current value is: ["Adrian E"]
//New value will be: ["Adrian E", "Allen W"]
//There are now 2 contacts.
//Old value was ["Adrian E"]
//Current value is: ["Adrian E", "Allen W"]
//New value will be: ["Adrian E", "Allen W", "Ish S"]
//There are now 3 contacts.
//Old value was ["Adrian E", "Allen W"]

didSet ve willSet kullanırken dikkatli olmak gerekir. Çünkü property observer ‘lara çok fazla iş yüklemek performans sorunlarına sebep olabilir.

Not : Property observer sabitler (let) ile kullanılmazlar. (Çünkü sabitlerin değeri yalnızca bir kez belirlenir)

Struct Özel Initializer Nasıl Oluşturulur? #

Initiliazer, kullanılacak yeni bir struct instance’ı hazırlamak için tasarlanmış özel method’lardır. Daha önce struct içerisine koyduğumuz property’lere göre nasıl otomatik olarak oluşturulduklarını görmüştük. Fakat kurala uyduğumuz sürece biz de kendi özel initializer’ımızı oluşturabiliriz. Olmazsa olmaz kuralımız: initializer sona erdiğinde tüm property’lerin bir değeri olmalıdır.

Otomatik oluşturulan initializer’a bakalım;

struct Player {
    let name: String
    let number: Int
}

let player = Player(name: "Megan R", number: 15)

Swift yeni bir Player instance’ı için mevcut iki property ile varsayılan olarak initializer oluşturur. Swift’te buna memberwise initializer denilmektedir.

İstersek init fonksiyonunu kendimiz de yazabiliriz.

struct Player {
    let name: String
    let number: Int

    init(name: String, number: Int) {
        self.name = name
        self.number = number
    }
}

Yukarıdaki kod, az önce yazdığımız kod ile aynı şeyi yapar. Fakat burada initializer bize aittir ve istersek işlevsellik ekleyebiliriz.

Dikkat etmemiz gereken bazı hususlar;

  • func keyword’ü yoktur. Sözdizimi açısından bir fonksiyon gibi görünüyor ancak Swift burada initializer’a bir ayrıcalıkta bulunuyor.
  • init yeni bir Player instance’ı oluştursa da, initializer’ların sabit bir dönüş tipi yoktur.
  • Struct’da kullanılan name ile init içerisinde kullanılan name ’i birbirinden ayırmak için self keyword’ünü kullanırız. (self.name property olandır.)

Elbette özel olarak oluşturduğumuz initializer’ın, otomatik oluşturulan memberwise initiliazer gibi çalışması gerekmez. Örneğin, name değişkeni mutlaka kullanıcı tarafından verilebilirken, number değişkeni rastgele atanabilir.

struct Player {
    let name: String
    let number: Int

    init(name: String) {
        self.name = name
        number = Int.random(in: 1...99)
    }
}

let player = Player(name: "Megan R")
print(player.number)

Initializer sona erdiğinde tüm property’lerin bir değeri olmalıdır. Eğer number değişkenine bir değer atanmasaydı yukarıdaki kod hata verecekti.

Struct’a birden fazla initializer ekleyebilir, harici parametre adı ve varsayılan değer gibi özelliklerden yararlanabiliriz. Şunu unutmamak gerekir; custom initializer oluşturduğumuzda, -aksi belirtilmedikçe- Swift tarafından otomatik oluşturulan memberwise initializer’a erişimimizi kaybederiz.

Memberwise Initializer ile Custom Initializer’ın Beraber Kullanımı #

Eğer custom initializer kullanırsak, memberwise initializer’ın kullanımının kalkacağından bahsetmiştik. Fakat Swift’de bu durumun istisnasını oluşturabiliriz.

İstisnayı oluşturmak için, extension kullanacağız.

struct Employee {
    var name: String
    var yearsActive = 0
}

extension Employee {
    init() {
        self.name = "Anonymous"
        print("Creating an anonymous employee…")
    }
}

// name değişkenini belirleyerek roslin i oluşturabiliriz.
let roslin = Employee(name: "Laura Roslin")

// herhangi bir değişkeni belirlemeden de anon değişkenini oluşturabiliriz.
// bu durumda name değişkeni Anonymous olacaktır.
let anon = Employee()

Bu yazıyı İngilizce olarak da okuyabilirsiniz.
You can also read this article in English.

Bu yazı, SwiftUI Day 10 adresinde bulunan yazılardan kendim için aldığım notları içermektedir. Orjinal dersi takip etmek için lütfen bağlantıya tıklayın.